package mosquito.g7.strategy;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.apache.log4j.Logger;

import mosquito.g7.Board;
import mosquito.g7.StaticZap;
import mosquito.g7.Board.Degree;
import mosquito.sim.Collector;
import mosquito.sim.Light;

class LightBind {
    private int numLightsAroundOtherLights;
    private int numLightsAroundCenterLight;
    private int startPosition;
    
    public LightBind(int numLightsAroundOtherLights, int numLightsAroundCenterLight, int startPosition) {
        this.numLightsAroundOtherLights = numLightsAroundOtherLights;
        this.numLightsAroundCenterLight = numLightsAroundCenterLight;
        this.startPosition = startPosition;
    }
    
    public boolean equals(Object obj) {
        LightBind lb = (LightBind) obj;
        return numLightsAroundOtherLights == lb.numLightsAroundOtherLights 
        &&  numLightsAroundCenterLight == lb.numLightsAroundCenterLight
        && startPosition == lb.startPosition;
    }
    
    public int hashCode() {
        return numLightsAroundOtherLights + 37*numLightsAroundCenterLight + 37*37*startPosition;
    }
}

class StrategyBind {
    private int numLightsAroundOtherLights;
    private int numLightsAroundCenterLight;
    private int offsetForLightTiming;
    private double flyingBuffer;
    private int startPosition;
    
    public StrategyBind(int numLightsAroundOtherLights, int numLightsAroundCenterLight, int offsetForLightTiming, double flyingBuffer, int startPosition) {
        this.numLightsAroundOtherLights = numLightsAroundOtherLights;
        this.numLightsAroundCenterLight = numLightsAroundCenterLight;
        this.offsetForLightTiming = offsetForLightTiming;
        this.flyingBuffer = flyingBuffer;
        this.startPosition = startPosition;
    }
    
    public boolean equals(Object obj) {
        StrategyBind sb = (StrategyBind) obj;
        return numLightsAroundOtherLights == sb.numLightsAroundOtherLights 
        &&  numLightsAroundCenterLight == sb.numLightsAroundCenterLight
        && offsetForLightTiming == sb.offsetForLightTiming
        && flyingBuffer == sb.flyingBuffer
        && startPosition == sb.startPosition;
    }
    
    public int hashCode() {
        return numLightsAroundOtherLights 
        + 37*numLightsAroundCenterLight 
        + 37*37*offsetForLightTiming 
        + (int)(37*37*37*flyingBuffer)
        + 37*37*37*37*startPosition;
    }
}


public class SimpleStrategy extends Strategy {

    private Board board;
    private int radiusDelta;
    private Point2D.Double collectorPoint;

    private int numLightsAroundCenterLight, numLightsAroundOtherLights, offsetForLightTiming, initialPosition;
    private double flyingBuffer;
    private Logger logger = Logger.getLogger(SimpleStrategy.class);
    
    private static HashMap<LightBind, ArrayList<ArrayList<Point2D.Double>>> previouslyPlacedLights;
    private static HashMap<StrategyBind, Set<Light>> previousLightSets;
    private static HashMap<LightBind, Collector> previousCollectors;

    LightBind lb;
    StrategyBind sb;
    
    public static void resetStaticComponents() {
        previouslyPlacedLights = new HashMap<LightBind, ArrayList<ArrayList<Point2D.Double>>>();
        previousLightSets = new HashMap<StrategyBind, Set<Light>>();
        previousCollectors = new HashMap<LightBind, Collector>();
    }
    
    public SimpleStrategy(Board board, int numLightsAroundCenterLight, int numLightsAroundOtherLights, int offsetForLightTiming, double flyingBuffer, int initialPosition){
        super();
        this.board = board;
        this.numLightsAroundCenterLight = numLightsAroundCenterLight;
        this.numLightsAroundOtherLights = numLightsAroundOtherLights;
        this.offsetForLightTiming = offsetForLightTiming;
        this.flyingBuffer = flyingBuffer;
        this.initialPosition = initialPosition;
        radiusDelta = StaticZap.RADIUS_DELTA;
        collectorPoint = null;
        sb = new StrategyBind(numLightsAroundOtherLights, numLightsAroundCenterLight, offsetForLightTiming, flyingBuffer, initialPosition);
        lb = new LightBind(numLightsAroundOtherLights, numLightsAroundCenterLight, initialPosition);
    }

    private Point2D.Double getCenterLight(int degree) {
        setCollectorPoint();
        return Degree.getPointDegreeAndDistanceFromPoint(new Point2D.Double(collectorPoint.getX(), collectorPoint.getY()), degree, .5);
    }

    private boolean isPointWithinRangeOfOtherLights(Point2D.Double point, ArrayList<Point2D.Double> otherLights, double range) {
        for(Point2D.Double light : otherLights) {
            if(light.distance(point) < range)
                return true;
        }
        return false;
    }

    private boolean willPlaceNewLight(Point2D.Double newLight, Point2D.Double centerLight, HashMap<Point2D.Double, Integer> angleFromLastLight, int numLightsFromCollector) {
        ArrayList<Point2D.Double> lightsToCompareToInSameChain = (ArrayList<Point2D.Double>) getLightsFromNthChainFromCollector(numLightsFromCollector).clone();
        ArrayList<Point2D.Double> lightsToCompareToInOtherChains = (ArrayList<Point2D.Double>) getLightsFromNthChainFromCollector(0).clone();
        for(int i = 1; i < numLightsFromCollector; i++)
            lightsToCompareToInOtherChains.addAll(getLightsFromNthChainFromCollector(i)); 
        lightsToCompareToInOtherChains.remove(centerLight);
        if(newLight != null 
                && !isPointWithinRangeOfOtherLights(newLight, lightsToCompareToInSameChain, (StaticZap.RADIUS / StaticZap.GRANULARITY) / 8)
                && !isPointWithinRangeOfOtherLights(newLight, lightsToCompareToInOtherChains, (StaticZap.RADIUS / StaticZap.GRANULARITY) / 8)) { //TODO: range should be smarter
            angleFromLastLight.put(newLight, (int)Math.round(Degree.getDegreeDifferenceBetweenPoints(newLight, centerLight)));
            addLightToNthChainFromCollector(newLight, numLightsFromCollector);
            return true;
        }
        return false;
    }

    private void setLightPositions(int numberOfLights) {
        HashMap<Point2D.Double, Integer> angleFromLastLight = new HashMap<Point2D.Double, Integer>();

        addLightToNthChainFromCollector(getCenterLight(Degree.S.getDegree()), 0); // TODO: actually get direction, also could be out of bounds
        int lightsPlaced = 1;
        int numLightsFromCollector = 0;
        int timeOutCounter = 0;
        
        while (lightsPlaced < numberOfLights)
        {
            if(timeOutCounter > numberOfLights *3)
                break;
            timeOutCounter++;
            
            numLightsFromCollector++;
            for(Point2D.Double centerLight : getLightsFromNthChainFromCollector(numLightsFromCollector-1)) { // for every light N lights from collector
                Point2D.Double newLight;
                if(numLightsFromCollector == 1) { // surround collector with lights
                    for(int i = 0; i < numLightsAroundCenterLight && lightsPlaced < numberOfLights; i++) {
                        logger.debug(centerLight.getX()+", " + centerLight.getY() + "\t" + (360/Math.min(numLightsAroundCenterLight, numberOfLights-1)) * i + " <--> " + (360/Math.min(numLightsAroundCenterLight, numberOfLights-1)) * (i+1));
                        newLight = board.getPointLeastInShadowWithinRadiiAndDegreeRangeFromPoint(centerLight.getX(), centerLight.getY(), 
                                StaticZap.RADIUS - radiusDelta, StaticZap.RADIUS, (360/Math.min(numLightsAroundCenterLight, numberOfLights-1)) * i, (360/Math.min(numLightsAroundCenterLight, numberOfLights-1)) * (i+1));
                        if(willPlaceNewLight(newLight, centerLight, angleFromLastLight, numLightsFromCollector))
                        {
                            lightsPlaced++;
                            logger.debug(newLight.getX()+", " + newLight.getY() + "\t" + (int)Math.round(Degree.getDegreeDifferenceBetweenPoints(newLight, centerLight)));
                            timeOutCounter = 0;
                        } else {
                            if(newLight != null) {
                                board.getPixel(newLight).neverPlaceLightHere();
                                timeOutCounter = 0;
                            }
                        }
                    }
                } else {
                    int firstAngle = angleFromLastLight.get(centerLight); // between -180 and 180 degrees
                    int baseAngle = firstAngle - StaticZap.DEGREES_TO_USE/2;
                    int fromDegree = baseAngle;
                    int toDegree = baseAngle + StaticZap.DEGREES_TO_USE;
                    if(!(fromDegree <= firstAngle && firstAngle <= toDegree)) { // make sure we are looking in the right degree range (we don't want to go back to where we came)
                        logger.debug("rotating from: " + fromDegree + " <--> " + toDegree + "\t" + (toDegree - 360) + " <--> " + fromDegree +"\tfirst angle " + firstAngle);
                        int placeHolder = fromDegree;
                        fromDegree = toDegree - 360;
                        toDegree = placeHolder;
                    } else {
                        logger.debug("not rotating from: " + fromDegree + " <--> " + toDegree + "\tfirstAngle is " + firstAngle);
                    }
                    
                    for(int i = 0; i < numLightsAroundOtherLights && lightsPlaced < numberOfLights; i++) {  // branch out away from previous lights
                        fromDegree = baseAngle + (StaticZap.DEGREES_TO_USE/numLightsAroundOtherLights) * i;
                        toDegree = baseAngle + (StaticZap.DEGREES_TO_USE/numLightsAroundOtherLights) * (i+1);
                        
                        logger.debug(centerLight.getX()+", " + centerLight.getY() + "\t" + fromDegree + " <--> " + toDegree);                        

                        newLight = board.getPointLeastInShadowWithinRadiiAndDegreeRangeFromPoint(centerLight.getX(), centerLight.getY(), StaticZap.RADIUS - radiusDelta, StaticZap.RADIUS, fromDegree, toDegree);
                        if(willPlaceNewLight(newLight, centerLight, angleFromLastLight, numLightsFromCollector))
                        {
                            lightsPlaced++;
                            logger.debug(newLight.getX()+", " + newLight.getY() + "\t" + (int)Math.round(Degree.getDegreeDifferenceBetweenPoints(newLight, centerLight)));
                        } else {
                            if(newLight != null) {
                                board.getPixel(newLight).neverPlaceLightHere();
                                timeOutCounter = 0;
                            }
                        }
                    }
                }
            }
        }
        for(lightsPlaced = lightsPlaced; lightsPlaced < numberOfLights; lightsPlaced++) {
            addLightToNthChainFromCollector(new Point2D.Double(0, 0), 0);
        }
    }        

    private Set<Light> translatePositionsIntoLightSet() {
        HashSet<Light> lights = new HashSet<Light>(); // the collection of lights we are returning
        ArrayList<Double> cycleDuration = new ArrayList<Double>(); // the length of the cycle duration N lights away from the center light   

        for(int numLightsFromCollector = 0; numLightsFromCollector < maxLightsFromCollector(); numLightsFromCollector++) {
            ArrayList<Point2D.Double> ringNAwayFromCenterLight = getLightsFromNthChainFromCollector(numLightsFromCollector);
            if(numLightsFromCollector == 0) {
                cycleDuration.add((double) maxLightsFromCollector()*StaticZap.FLYING_RADIUS_TIME); //position 0
                lights.add(new Light(ringNAwayFromCenterLight.get(0).getX(), ringNAwayFromCenterLight.get(0).getY(), cycleDuration.get(numLightsFromCollector), cycleDuration.get(numLightsFromCollector), 0)); //TODO: what if multiple lights RIGHT about the collector?
                for(int i = 1; i < ringNAwayFromCenterLight.size(); i++){
                    lights.add(new Light(ringNAwayFromCenterLight.get(i).getX(), ringNAwayFromCenterLight.get(i).getY(), 0, 0, 0));
                }
            } else if((numLightsFromCollector + offsetForLightTiming) % 2 == 1)  { // odd
                for(Point2D.Double lightPoint : ringNAwayFromCenterLight) {
                    lights.add(new Light(lightPoint.getX(), lightPoint.getY(), 2*StaticZap.FLYING_RADIUS_TIME, StaticZap.FLYING_RADIUS_TIME, StaticZap.FLYING_RADIUS_TIME+numLightsFromCollector*flyingBuffer));
                }
            } else { // even
                for(Point2D.Double lightPoint : ringNAwayFromCenterLight) {
                    lights.add(new Light(lightPoint.getX(), lightPoint.getY(), 2*StaticZap.FLYING_RADIUS_TIME, StaticZap.FLYING_RADIUS_TIME, 0+numLightsFromCollector*flyingBuffer));
                }
            }
        }
        return lights;
    }

    public Set<Light> getLights(int numberOfLights) {        
        if(!previouslyPlacedLights.containsKey(lb)) {
            setLightPositions(numberOfLights);
            previouslyPlacedLights.put(lb, placedLights);
        } else
            placedLights = previouslyPlacedLights.get(lb);
        
        if(!previousLightSets.containsKey(sb)) {
            previousLightSets.put(sb, translatePositionsIntoLightSet());
        }
        
        return previousLightSets.get(sb);
    }

    public void setCollectorPoint() {        
        if(!previousCollectors.containsKey(lb)) {
            collectorPoint = board.getFurthestPointAwayFromLinesAndEdgeBoundaries(StaticZap.NUMBER_OF_CUTS, StaticZap.MAX_NUMBER_START_POSITIONS)[initialPosition];
            Collector c = new Collector(collectorPoint.getX(), collectorPoint.getY());
            previousCollectors.put(lb, c);
        }
    }
    
    public Collector getCollector() {
        setCollectorPoint();
        return previousCollectors.get(lb);
    }
}
